﻿using System;

namespace Cash4net
{
    public class Emacs
    {
        /// <summary>
        /// Returns the depreciation of an asset for a specified period by using the double-declining balance method or some other method that you specify.
        /// </summary>
        /// <param name="cost">The initial cost of the asset.</param>
        /// <param name="salv">The value at the end of the depreciation.</param>
        /// <param name="life">The number of periods over which the asset is being depreciated.</param>
        /// <param name="per">The period for which you want to calculate the depreciation.</param>
        /// <param name="factor">The rate at which the balance declines. If factor is omitted, it is assumed to be 2.</param>
        /// <returns>Depreciation of an asset for a specified period.</returns>
        public decimal ddb(decimal cost, decimal salv, decimal life, decimal per, decimal factor = 2m)
        {
            if (cost <= 0) throw new ArgumentOutOfRangeException("Cost value must be greater than zero.");
            if (salv < 0) throw new ArgumentOutOfRangeException("Salvage value must be greater or equal zero.");
            if (life <= 0) throw new ArgumentOutOfRangeException("Life value must be greater than zero.");
            if (per <= 0) throw new ArgumentOutOfRangeException("Period value must be greater than zero.");
            if (factor <= 0) throw new ArgumentOutOfRangeException("Factor value must be greater than zero.");

            double tmp1 = Convert.ToDouble((life - factor) / life);
            double tmp2 = Convert.ToDouble(per - 1m);
            double tmp3 = Convert.ToDouble(per);
            decimal d1 = factor * cost * Convert.ToDecimal(Math.Pow(tmp1, tmp2)) / life;
            decimal d2 = (factor * cost * Convert.ToDecimal(Math.Pow(tmp1, tmp2)) / life) -
                ((1m - Convert.ToDecimal(Math.Pow(tmp1, tmp3))) * cost - (cost - salv));
            return Math.Min(d1, d2);
        }

        /// <summary>
        /// The future value of an investment based on periodic, constant payments and a constant interest rate.
        /// </summary>
        /// <param name="rate">The interest rate per period.</param>
        /// <param name="n">The total number of payment periods in an annuity.</param>
        /// <param name="pmt">The payment made each period.</param>
        /// <returns>Future value of an investment.</returns>
        public decimal fv(decimal rate, int n, decimal pmt)
        {
            if (rate == 0) throw new ArgumentOutOfRangeException("Rate value must not be zero.");
            if (n < 0) throw new ArgumentOutOfRangeException("Number of periods must be greater or equal zero.");

            decimal tmp = _pow(1m + rate, n) - 1m;
            return pmt * tmp / rate;
        }

        public decimal fvb(decimal rate, int n, decimal pmt)
        {
            if (rate == 0) throw new ArgumentOutOfRangeException("Rate value must not be zero.");
            if (n < 0) throw new ArgumentOutOfRangeException("Number of periods must be greater or equal zero.");

            decimal tmp = (_pow(1m + rate, n) - 1m) * (1m + rate);
            return pmt * tmp / rate;
        }

        public decimal fvl(decimal rate, int n, decimal pmt)
        {
            if (rate == 0) throw new ArgumentOutOfRangeException("Rate value must not be zero.");
            if (n < 0) throw new ArgumentOutOfRangeException("Number of periods must be greater or equal zero.");

            decimal tmp = _pow(1m + rate, n);
            return pmt * tmp;
        }

        /// <summary>
        /// The number of periods for an investment based on periodic, constant payments and a constant interest rate.
        /// </summary>
        /// <param name="rate">The interest rate per period.</param>
        /// <param name="pmt">The payment made each period.</param>
        /// <param name="amt">The lump-sum amount that a series of future payments is worth right now.</param>
        /// <returns>Number of periods for an investment.</returns>
        public decimal nper(decimal rate, decimal pmt, decimal amt)
        {
            if (rate == 0) throw new ArgumentOutOfRangeException("Rate value must not be zero.");
            if (pmt == 0) throw new ArgumentOutOfRangeException("Payment value must not be zero.");

            double tmp1 = Convert.ToDouble(1m - ((amt * rate) / pmt));
            double tmp2 = Convert.ToDouble(1m + rate);
            return -Convert.ToDecimal(Math.Log(tmp1, tmp2));
        }

        public decimal nperb(decimal rate, decimal pmt, decimal amt)
        {
            if (rate == 0) throw new ArgumentOutOfRangeException("Rate value must not be zero.");
            if (pmt == 0) throw new ArgumentOutOfRangeException("Payment value must not be zero.");

            double tmp1 = Convert.ToDouble(1m - ((amt * rate) / (pmt * (1m + rate))));
            double tmp2 = Convert.ToDouble(1m + rate);
            return -Convert.ToDecimal(Math.Log(tmp1, tmp2));
        }

        public decimal nperl(decimal rate, decimal pmt, decimal amt)
        {
            if (rate == 0) throw new ArgumentOutOfRangeException("Rate value must not be zero.");
            if (pmt == 0) throw new ArgumentOutOfRangeException("Payment value must not be zero.");

            double tmp1 = Convert.ToDouble(amt / pmt);
            double tmp2 = Convert.ToDouble(1m + rate);
            return -Convert.ToDecimal(Math.Log(tmp1, tmp2));
        }

        /// <summary>
        ///  The net present value of an investment by using a discount rate and a series of future payments (negative values) and income (positive values).
        /// </summary>
        /// <param name="rate">The rate of discount over the length of one period.</param>
        /// <param name="a">1st argument representing the payments and income.</param>
        /// <param name="b">2nd argument representing the payments and income.</param>
        /// <param name="c">3rd argument representing the payments and income.</param>
        /// <returns>Net present value of an investment.</returns>
        public decimal npv(decimal rate, decimal a, decimal b = 0, decimal c = 0)
        {
            if (rate == 0) throw new ArgumentOutOfRangeException("Rate value must not be zero.");

            decimal tmp1 = a * (1m / _pow(1m + rate, 1));
            decimal tmp2 = b * (1m / _pow(1m + rate, 2));
            decimal tmp3 = c * (1m / _pow(1m + rate, 3));
            return tmp1 + tmp2 + tmp3;
        }

        public decimal npvb(decimal rate, decimal a, decimal b = 0, decimal c = 0)
        {
            if (rate == 0) throw new ArgumentOutOfRangeException("Rate value must not be zero.");

            decimal tmp1 = a;
            decimal tmp2 = b * (1m / _pow(1m + rate, 1));
            decimal tmp3 = c * (1m / _pow(1m + rate, 2));
            return tmp1 + tmp2 + tmp3;
        }

        /// <summary>
        /// The payment for a loan based on constant payments and a constant interest rate.
        /// </summary>
        /// <param name="rate">The interest rate for the loan.</param>
        /// <param name="n">The total number of payments for the loan.</param>
        /// <param name="amt">The total amount that a series of future payments is worth now; also known as the principal.</param>
        /// <param name="x">The future value, or a cash balance you want to attain after the last payment is made.</param>
        /// <returns>Payment for a loan based on constant payments and a constant interest rate.</returns>
        public decimal pmt(decimal rate, int n, decimal amt, decimal x = 0)
        {
            if (rate == 0) throw new ArgumentOutOfRangeException("Rate value must not be zero.");
            if (n <= 0) throw new ArgumentOutOfRangeException("Number of payments must be greater than zero.");

            decimal tmp1 = (amt - x * (1m / _pow(1m + rate, n))) * rate;
            decimal tmp2 = 1m - (1m / _pow(1m + rate, n));
            return tmp1 / tmp2;
        }

        public decimal pmtb(decimal rate, int n, decimal amt, decimal x)
        {
            if (rate == 0) throw new ArgumentOutOfRangeException("Rate value must not be zero.");
            if (n <= 0) throw new ArgumentOutOfRangeException("Number of payments must be greater than zero.");

            decimal tmp1 = (amt - x * (1m / _pow(1m + rate, n))) * rate;
            decimal tmp2 = (1m - (1m / _pow(1m + rate, n))) * (1m + rate);
            return tmp1 / tmp2;
        }

        /// <summary>
        /// The present value of an investment. 
        /// </summary>
        /// <param name="rate">The interest rate per period.</param>
        /// <param name="n">The total number of payment periods in an annuity.</param>
        /// <param name="pmt">The payment made each period and cannot change over the life of the annuity.</param>
        /// <returns>Present value of an investment.</returns>
        public decimal pv(decimal rate, int n, decimal pmt)
        {
            if (rate == 0) throw new ArgumentOutOfRangeException("Rate value must not be zero.");
            if (n < 0) throw new ArgumentOutOfRangeException("Number of payments must be greater or equal zero.");

            decimal tmp = 1m - (1m / _pow(1m + rate, n));
            return pmt * tmp / rate;
        }

        public decimal pvb(decimal rate, int n, decimal pmt)
        {
            if (rate == 0) throw new ArgumentOutOfRangeException("Rate value must not be zero.");
            if (n < 0) throw new ArgumentOutOfRangeException("Number of payments must be greater or equal zero.");

            decimal tmp = (1m - (1m / _pow(1m + rate, n))) * (1m + rate);
            return pmt * tmp / rate;
        }

        public decimal pvl(decimal rate, int n, decimal pmt)
        {
            if (rate == 0) throw new ArgumentOutOfRangeException("Rate value must not be zero.");
            if (n < 0) throw new ArgumentOutOfRangeException("Number of payments must be greater or equal zero.");

            decimal tmp = 1m / _pow(1m + rate, n);
            return pmt * tmp;
        }

        /// <summary>
        /// The interest rate per period of an annuity.
        /// </summary>
        /// <param name="n">The total number of payment periods in an annuity.</param>
        /// <param name="pmt">The payment made each period and cannot change over the life of the annuity.</param>
        /// <param name="amt">The total amount that a series of future payments is worth now.</param>
        /// <returns>Interest rate per period of an annuity.</returns>
        public decimal ratel(int n, decimal pmt, decimal amt)
        {
            if (n <= 0) throw new ArgumentOutOfRangeException("Number of payment periods must be greater than zero.");

            double tmp1 = Math.Pow(Convert.ToDouble(pmt), 1.0 / n);
            double tmp2 = Math.Pow(Convert.ToDouble(amt), 1.0 / n);
            return (Convert.ToDecimal(tmp1) / Convert.ToDecimal(tmp2)) - 1m;
        }

        /// <summary>
        /// The straight-line depreciation of an asset for one period.
        /// </summary>
        /// <param name="cost">The initial cost of the asset.</param>
        /// <param name="salv">The value at the end of the depreciation.</param>
        /// <param name="life">The number of periods over which the asset is depreciated.</param>
        /// <returns>Straight-line depreciation of an asset.</returns>
        public decimal sln(decimal cost, decimal salv, decimal life)
        {
            if (life <= 0) throw new ArgumentOutOfRangeException("Number of periods value must be greater than zero.");

            return (cost - salv) / life;
        }

        /// <summary>
        /// The sum-of-years' digits depreciation of an asset for a specified period.
        /// </summary>
        /// <param name="cost">The initial cost of the asset.</param>
        /// <param name="salv">The value at the end of the depreciation.</param>
        /// <param name="life">The number of periods over which the asset is depreciated.</param>
        /// <param name="per">The period and must use the same units as life.</param>
        /// <returns>Sum-of-years' digits depreciation of an asset.</returns>
        public decimal syd(decimal cost, decimal salv, decimal life, decimal per)
        {
            if (life <= 0) throw new ArgumentOutOfRangeException("Number of periods value must be greater than zero.");

            decimal tmp1 = (cost - salv) * (life - per + 1m);
            decimal tmp2 = life * (life + 1m) / 2m;
            return tmp1 / tmp2;
        }


        private static decimal _pow(decimal @base, int exp)
        {
            if (exp == 0) return 1m;
            if (exp == 1) return @base;

            decimal power = @base;
            for (decimal iteration = 1m; iteration < exp; iteration += 1M)
                power *= @base;

            return power;
        }
    }
}
